package hibernatemock;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import lombok.Getter;
import lombok.Setter;

public class InstanceFactory {

	@Setter
	@Getter
	private List<Setup> setups;
	private List<Field> fields = new ArrayList<Field>();

	public Object createInstance(Class<?> clazz, List<Annotation> annotations, Field field) {
		Class<?> realClass = getRealClass(clazz);
		List<Setup> options = optionsWithoutObject(realClass);
		if (options.size() == 0) {
			return newInstance(realClass);
		} else {
			options = optionsWithObject(realClass);
			List<Class<? extends Annotation>> listTemp = parse(annotations);

			for (Setup setup : options)
				if (listTemp.contains(setup.getClazzAnnotation()))
					return setup.getInstance(annotations);

			Setup temp = null;
			for (Setup fieldSetup : options) {
				if (fieldSetup.getClazzAnnotation() == null) {
					temp = fieldSetup;
					break;
				}
			}
			
			setups.remove(temp);
			setups.add(temp);

			if (fields.contains(field))
				return null;
			
			if (temp != null)
				return temp.getInstance(annotations);

			throw new RuntimeException("Could not instaniate "
					+ clazz.getName());
		}
	}

	public void addNullField(Field field) {
		fields.add(field);
	}

	private List<Class<? extends Annotation>> parse(List<Annotation> annotations) {
		List<Class<? extends Annotation>> list = new ArrayList<Class<? extends Annotation>>();
		if (annotations != null) {
			for (Annotation ann : annotations) {
				list.add(ann.annotationType());
			}
		}
		return list;
	}

	private List<Setup> optionsWithObject(Class<?> clazz) {
		List<Setup> options = new ArrayList<Setup>();
		for (Setup setup : setups)
			if (setup.getClazzField() == clazz
					|| setup.getClazzField() == Object.class)
				options.add(setup);
		return options;
	}

	private List<Setup> optionsWithoutObject(Class<?> clazz) {
		List<Setup> options = new ArrayList<Setup>();
		for (Setup setup : setups)
			if (setup.getClazzField() == clazz)
				options.add(setup);
		return options;
	}

	private Class<?> getRealClass(Class<?> clazz) {
		if (clazz.isPrimitive()) {
			String name = clazz.getSimpleName();
			name = name.substring(0, 1).toUpperCase() + name.substring(1);
			if (name.equals("Char"))
				name = "Character";
			if (name.equals("Int"))
				name = "Integer";
			return primitiveClass("java.lang." + name);
		}
		return clazz;
	}

	private Class<?> primitiveClass(String name) {
		try {
			return Class.forName(name);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings("unchecked")
	public Object newInstance(Class<?> clazz) {
		try {
			if (clazz.isEnum())
				return Enum.valueOf((Class) clazz,
						clazz.getDeclaredFields()[0].getName());
			return clazz.newInstance();
		} catch (Exception e) {
			for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
				try {
					return constructor
							.newInstance(getConstructorParams(constructor));
				} catch (Exception e1) {
					// trying another constructor
				}
			}
		}
		throw new RuntimeException(
				"Is "
						+ clazz.getSimpleName()
						+ " an abstract class or interface? Try setup hibernateMock.when("
						+ clazz.getSimpleName() + ").deliver(?)");
	}

	private Object[] getConstructorParams(Constructor<?> constructor) {
		List<Object> constructorParams = new ArrayList<Object>();
		for (Class<?> parameterClass : constructor.getParameterTypes())
			constructorParams.add(createInstance(parameterClass, null, null));
		return constructorParams.toArray();
	}

}
